Completed
Pull Request — master (#91)
by thomas
01:27
created

wallet.js ➔ readBech32Address   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
nc 6
dl 0
loc 20
rs 9.2
nop 2
1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param segwit                int             segwit toggle from server
32
 * @param testnet               bool            testnet
33
 * @param checksum              string
34
 * @param upgradeToKeyIndex     int
35
 * @param useNewCashAddr        bool            flag to opt in to bitcoin cash cashaddr's
36
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
37
 * @constructor
38
 * @internal
39
 */
40
var Wallet = function(
41
    sdk,
42
    identifier,
43
    walletVersion,
44
    primaryMnemonic,
45
    encryptedPrimarySeed,
46
    encryptedSecret,
47
    primaryPublicKeys,
48
    backupPublicKey,
49
    blocktrailPublicKeys,
50
    keyIndex,
51
    segwit,
52
    testnet,
53
    checksum,
54
    upgradeToKeyIndex,
55
    useNewCashAddr,
56
    bypassNewAddressCheck
57
) {
58
    /* jshint -W071 */
59
    var self = this;
60
61
    self.sdk = sdk;
62
    self.identifier = identifier;
63
    self.walletVersion = walletVersion;
64
    self.locked = true;
65
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
66
    self.bitcoinCash = self.sdk.bitcoinCash;
67
    self.segwit = !!segwit;
68
    self.useNewCashAddr = !!useNewCashAddr;
69
    assert(!self.segwit || !self.bitcoinCash);
70
71
    self.testnet = testnet;
72
    if (self.bitcoinCash) {
73
        if (self.testnet) {
74
            self.network = bitcoin.networks.bitcoincashtestnet;
75
        } else {
76
            self.network = bitcoin.networks.bitcoincash;
77
        }
78
    } else {
79
        if (self.testnet) {
80
            self.network = bitcoin.networks.testnet;
81
        } else {
82
            self.network = bitcoin.networks.bitcoin;
83
        }
84
    }
85
86
    assert(backupPublicKey instanceof bitcoin.HDNode);
87
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
88
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
89
90
    // v1
91
    self.primaryMnemonic = primaryMnemonic;
92
93
    // v2 & v3
94
    self.encryptedPrimarySeed = encryptedPrimarySeed;
95
    self.encryptedSecret = encryptedSecret;
96
97
    self.primaryPrivateKey = null;
98
    self.backupPrivateKey = null;
99
100
    self.backupPublicKey = backupPublicKey;
101
    self.blocktrailPublicKeys = blocktrailPublicKeys;
102
    self.primaryPublicKeys = primaryPublicKeys;
103
    self.keyIndex = keyIndex;
104
105
    if (!self.bitcoinCash) {
106
        if (self.segwit) {
107
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
108
            self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
109
        } else {
110
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
111
            self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
112
        }
113
    } else {
114
        self.chain = Wallet.CHAIN_BCC_DEFAULT;
115
        self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
116
    }
117
118
    self.checksum = checksum;
119
    self.upgradeToKeyIndex = upgradeToKeyIndex;
120
121
    self.secret = null;
122
    self.seedHex = null;
123
};
124
125
Wallet.WALLET_VERSION_V1 = 'v1';
126
Wallet.WALLET_VERSION_V2 = 'v2';
127
Wallet.WALLET_VERSION_V3 = 'v3';
128
129
Wallet.WALLET_ENTROPY_BITS = 256;
130
131
Wallet.OP_RETURN = 'opreturn';
132
Wallet.DATA = Wallet.OP_RETURN; // alias
133
134
Wallet.PAY_PROGRESS_START = 0;
135
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
136
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
137
Wallet.PAY_PROGRESS_SIGN = 30;
138
Wallet.PAY_PROGRESS_SEND = 40;
139
Wallet.PAY_PROGRESS_DONE = 100;
140
141
Wallet.CHAIN_BTC_DEFAULT = 0;
142
Wallet.CHAIN_BTC_SEGWIT = 2;
143
Wallet.CHAIN_BCC_DEFAULT = 1;
144
145
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
146
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
147
Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY;
148
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
149
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
150
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
151
152
Wallet.prototype.isSegwit = function() {
153
    return !!this.segwit;
154
};
155
156
Wallet.prototype.unlock = function(options, cb) {
157
    var self = this;
158
159
    var deferred = q.defer();
160
    deferred.promise.nodeify(cb);
161
162
    // avoid modifying passed options
163
    options = _.merge({}, options);
164
165
    q.fcall(function() {
166
        switch (self.walletVersion) {
167
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
168
                return self.unlockV1(options);
169
170
            case Wallet.WALLET_VERSION_V2:
171
                return self.unlockV2(options);
172
173
            case Wallet.WALLET_VERSION_V3:
174
                return self.unlockV3(options);
175
176
            default:
177
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
178
        }
179
    }).then(
180
        function(primaryPrivateKey) {
181
            self.primaryPrivateKey = primaryPrivateKey;
182
183
            // create a checksum of our private key which we'll later use to verify we used the right password
184
            var checksum = self.primaryPrivateKey.getAddress();
185
186
            // check if we've used the right passphrase
187
            if (checksum !== self.checksum) {
188
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
189
                    "[" + self.checksum + "], most likely due to incorrect password");
190
            }
191
192
            self.locked = false;
193
194
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
195
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
196
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
197
            }
198
        }
199
    ).then(
200
        function(r) {
201
            deferred.resolve(r);
202
        },
203
        function(e) {
204
            deferred.reject(e);
205
        }
206
    );
207
208
    return deferred.promise;
209
};
210
211
Wallet.prototype.unlockV1 = function(options) {
212
    var self = this;
213
214
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
215
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
216
217
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
218
        .then(function(options) {
219
            self.primarySeed = options.primarySeed;
220
221
            return options.primaryPrivateKey;
222
        });
223
};
224
225
Wallet.prototype.unlockV2 = function(options, cb) {
226
    var self = this;
227
228
    var deferred = q.defer();
229
    deferred.promise.nodeify(cb);
230
231
    deferred.resolve(q.fcall(function() {
232
        /* jshint -W071, -W074 */
233
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
234
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
235
236
        if (options.secret) {
237
            self.secret = options.secret;
238
        }
239
240
        if (options.primaryPrivateKey) {
241
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
242
        }
243
244
        if (options.primarySeed) {
245
            self.primarySeed = options.primarySeed;
246
        } else if (options.secret) {
247
            try {
248
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
249
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
250
                if (!self.primarySeed.length) {
251
                    throw new Error();
252
                }
253
            } catch (e) {
254
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
255
            }
256
257
        } else {
258
            // avoid conflicting options
259
            if (options.passphrase && options.password) {
260
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
261
            }
262
            // normalize passphrase/password
263
            options.passphrase = options.passphrase || options.password;
264
265
            try {
266
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
267
                if (!self.secret.length) {
268
                    throw new Error();
269
                }
270
            } catch (e) {
271
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
272
            }
273
            try {
274
                self.primarySeed = new Buffer(
275
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
276
                if (!self.primarySeed.length) {
277
                    throw new Error();
278
                }
279
            } catch (e) {
280
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
281
            }
282
        }
283
284
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
285
    }));
286
287
    return deferred.promise;
288
};
289
290
Wallet.prototype.unlockV3 = function(options, cb) {
291
    var self = this;
292
293
    var deferred = q.defer();
294
    deferred.promise.nodeify(cb);
295
296
    deferred.resolve(q.fcall(function() {
297
        return q.when()
298
            .then(function() {
299
                /* jshint -W071, -W074 */
300
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
301
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
302
303
                if (options.secret) {
304
                    self.secret = options.secret;
305
                }
306
307
                if (options.primaryPrivateKey) {
308
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
309
                }
310
311
                if (options.primarySeed) {
312
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
313
                } else if (options.secret) {
314
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
315
                        .then(function(primarySeed) {
316
                            self.primarySeed = primarySeed;
317
                        }, function() {
318
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
319
                        });
320
                } else {
321
                    // avoid conflicting options
322
                    if (options.passphrase && options.password) {
323
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
324
                    }
325
                    // normalize passphrase/password
326
                    options.passphrase = options.passphrase || options.password;
327
                    delete options.password;
328
329
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
330
                        .then(function(secret) {
331
                            self.secret = secret;
332
                        }, function() {
333
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
334
                        })
335
                        .then(function() {
336
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
337
                                .then(function(primarySeed) {
338
                                    self.primarySeed = primarySeed;
339
                                }, function() {
340
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
341
                                });
342
                        });
343
                }
344
            })
345
            .then(function() {
346
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
347
            })
348
        ;
349
    }));
350
351
    return deferred.promise;
352
};
353
354
Wallet.prototype.lock = function() {
355
    var self = this;
356
357
    self.secret = null;
358
    self.primarySeed = null;
359
    self.primaryPrivateKey = null;
360
    self.backupPrivateKey = null;
361
362
    self.locked = true;
363
};
364
365
/**
366
 * upgrade wallet to V3 encryption scheme
367
 *
368
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
369
 * @param cb
370
 * @returns {promise}
371
 */
372
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
373
    var self = this;
374
375
    var deferred = q.defer();
376
    deferred.promise.nodeify(cb);
377
378
    q.when(true)
379
        .then(function() {
380
            if (self.locked) {
381
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
382
            }
383
384
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
385
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
386
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
387
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
388
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
389
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
390
            }
391
        })
392
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
393
394
    return deferred.promise;
395
};
396
397
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
398
    var self = this;
399
400
    return q.when(true)
401
        .then(function() {
402
            var options = {
403
                storeDataOnServer: true,
404
                passphrase: passphrase,
405
                primarySeed: self.primarySeed,
406
                recoverySecret: false // don't create new recovery secret, V2 already has ones
407
            };
408
409
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
410
                .then(function(options) {
411
                    return self.sdk.updateWallet(self.identifier, {
412
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
413
                        encrypted_secret: options.encryptedSecret.toString('base64'),
414
                        wallet_version: Wallet.WALLET_VERSION_V3
415
                    }).then(function() {
416 View Code Duplication
                        self.secret = options.secret;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
417
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
418
                        self.encryptedSecret = options.encryptedSecret;
419
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
420
421
                        return self;
422
                    });
423
                });
424
        });
425
426
};
427
428
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
429
    var self = this;
430
431
    return q.when(true)
432
        .then(function() {
433
            var options = {
434
                storeDataOnServer: true,
435
                passphrase: passphrase,
436
                primarySeed: self.primarySeed
437
            };
438
439
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
440
                .then(function(options) {
441
                    // store recoveryEncryptedSecret for printing on backup sheet
442
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
443
444
                    return self.sdk.updateWallet(self.identifier, {
445
                        primary_mnemonic: '',
446
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
447
                        encrypted_secret: options.encryptedSecret.toString('base64'),
448
                        recovery_secret: options.recoverySecret.toString('hex'),
449
                        wallet_version: Wallet.WALLET_VERSION_V3
450
                    }).then(function() {
451
                        self.secret = options.secret;
452
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
453
                        self.encryptedSecret = options.encryptedSecret;
454
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
455
456
                        return self;
457
                    });
458
                });
459
        });
460
};
461
462
Wallet.prototype.doPasswordChange = function(newPassword) {
463
    var self = this;
464
465
    return q.when(null)
466
        .then(function() {
467
468
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
469
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
470
            }
471
472
            if (self.locked) {
473
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
474
            }
475
476
            if (!self.secret) {
477
                throw new blocktrail.WalletLockedError("No secret");
478
            }
479
480
            var newEncryptedSecret;
481
            var newEncrypedWalletSecretMnemonic;
482
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
483
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
484
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
485
486
            } else {
487
                if (typeof newPassword === "string") {
488
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
489
                } else {
490
                    if (!(newPassword instanceof Buffer)) {
491
                        throw new Error('New password must be provided as a string or a Buffer');
492
                    }
493
                }
494
495
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
496
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
497
498
                // It's a buffer, so convert it back to base64
499
                newEncryptedSecret = newEncryptedSecret.toString('base64');
500
            }
501
502
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
503
        });
504
};
505
506
Wallet.prototype.passwordChange = function(newPassword, cb) {
507
    var self = this;
508
509
    var deferred = q.defer();
510
    deferred.promise.nodeify(cb);
511
512
    q.fcall(function() {
513
        return self.doPasswordChange(newPassword)
514
            .then(function(r) {
515
                var newEncryptedSecret = r[0];
516
                var newEncrypedWalletSecretMnemonic = r[1];
517
518
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
519
                    self.encryptedSecret = newEncryptedSecret;
520
521
                    // backupInfo
522
                    return {
523
                        encryptedSecret: newEncrypedWalletSecretMnemonic
524
                    };
525
                });
526
            })
527
            .then(
528
                function(r) {
529
                    deferred.resolve(r);
530
                },
531
                function(e) {
532
                    deferred.reject(e);
533
                }
534
            );
535
    });
536
537
    return deferred.promise;
538
};
539
540
/**
541
 * get address for specified path
542
 *
543
 * @param path
544
 * @returns string
545
 */
546
Wallet.prototype.getAddressByPath = function(path) {
547
    return this.getWalletScriptByPath(path).address;
548
};
549
550
/**
551
 * get redeemscript for specified path
552
 *
553
 * @param path
554
 * @returns {Buffer}
555
 */
556
Wallet.prototype.getRedeemScriptByPath = function(path) {
557
    return this.getWalletScriptByPath(path).redeemScript;
558
};
559
560
/**
561
 * Generate scripts, and address.
562
 * @param path
563
 * @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}}
564
 */
565
Wallet.prototype.getWalletScriptByPath = function(path) {
566
    var self = this;
567
568
    // get derived primary key
569
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
570
    // get derived blocktrail key
571
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
572
    // derive the backup key
573
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
574
575
    // sort the pubkeys
576
    var pubKeys = Wallet.sortMultiSigKeys([
577
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
578
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
579
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
580
    ]);
581
582
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
583
    var scriptType = parseInt(path.split("/")[2]);
584
585
    var ws, rs;
586
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
587
        ws = multisig;
588
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
589
    } else {
590
        ws = null;
591
        rs = multisig;
592
    }
593
594
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
595
    var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr);
596
597
    return {
598
        witnessScript: ws,
599
        redeemScript: rs,
600
        scriptPubKey: spk,
601
        address: addr
602
    };
603
};
604
605
/**
606
 * get primary public key by path
607
 *  first level of the path is used as keyIndex to find the correct key in the dict
608
 *
609
 * @param path  string
610
 * @returns {bitcoin.HDNode}
611
 */
612
Wallet.prototype.getPrimaryPublicKey = function(path) {
613
    var self = this;
614
615
    path = path.replace("m", "M");
616
617
    var keyIndex = path.split("/")[1].replace("'", "");
618
619
    if (!self.primaryPublicKeys[keyIndex]) {
620
        if (self.primaryPrivateKey) {
621
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
622
        } else {
623
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
624
        }
625
    }
626
627
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
628
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
629
};
630
631
/**
632
 * get blocktrail public key by path
633
 *  first level of the path is used as keyIndex to find the correct key in the dict
634
 *
635
 * @param path  string
636
 * @returns {bitcoin.HDNode}
637
 */
638
Wallet.prototype.getBlocktrailPublicKey = function(path) {
639
    var self = this;
640
641
    path = path.replace("m", "M");
642
643
    var keyIndex = path.split("/")[1].replace("'", "");
644
645
    if (!self.blocktrailPublicKeys[keyIndex]) {
646
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
647
    }
648
649
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
650
651
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
652
};
653
654
/**
655
 * upgrade wallet to different blocktrail cosign key
656
 *
657
 * @param keyIndex  int
658
 * @param [cb]      function
659
 * @returns {q.Promise}
660
 */
661
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
662
    var self = this;
663
664
    var deferred = q.defer();
665
    deferred.promise.nodeify(cb);
666
667
    if (self.locked) {
668
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
669
        return deferred.promise;
670
    }
671
672
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
673
674
    deferred.resolve(
675
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
676
            .then(function(result) {
677
                self.keyIndex = keyIndex;
678
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
679
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
680
                });
681
682
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
683
684
                return true;
685
            })
686
    );
687
688
    return deferred.promise;
689
};
690
691
/**
692
 * generate a new derived private key and return the new address for it
693
 *
694
 * @param [chainIdx] int
695
 * @param [cb]  function        callback(err, address)
696
 * @returns {q.Promise}
697
 */
698
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
699
    var self = this;
700
701
    // chainIdx is optional
702
    if (typeof chainIdx === "function") {
703
        cb = chainIdx;
704
        chainIdx = null;
705
    }
706
707
    var deferred = q.defer();
708
    deferred.promise.spreadNodeify(cb);
709
710
    // Only enter if it's not an integer
711
    if (chainIdx !== parseInt(chainIdx, 10)) {
712
        // deal with undefined or null, assume defaults
713
        if (typeof chainIdx === "undefined" || chainIdx === null) {
714
            chainIdx = self.chain;
715
        } else {
716
            // was a variable but not integer
717
            deferred.reject(new Error("Invalid chain index"));
718
            return deferred.promise;
719
        }
720
    }
721
722
    deferred.resolve(
723
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
724
            .then(function(newDerivation) {
725
                var path = newDerivation.path;
726
                var addressFromServer = newDerivation.address;
727
728
                if (!self.bypassNewAddressCheck) {
729
                    var decodedFromServer;
730
731
                    try {
732
                        // Decode the address the serer gave us
733
                        decodedFromServer = self.decodeAddress(addressFromServer);
734
                    } catch (e) {
735
                        throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]");
736
                    }
737
738
                    // We need to reproduce this address with the same path,
739
                    // but the server (for BCH cashaddrs) uses base58?
740
                    var verifyAddress = self.getAddressByPath(newDerivation.path);
741
742
                    // If this occasion arises:
743
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
744
                        // Decode our the address we produced for the path
745
                        var decodeOurs;
746
                        try {
747
                            decodeOurs = self.decodeAddress(verifyAddress);
748
                        } catch (e) {
749
                            throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]");
750
                        }
751
752
                        // Peek beyond the encoding - the hashes must match at least
753
                        if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) {
754
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]");
755
                        }
756
757
                        var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH &&
758
                            decodedFromServer.decoded.version === self.network.pubKeyHash;
759
                        var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH &&
760
                            decodedFromServer.decoded.version === self.network.scriptHash;
761
762
                        if (!(matchedP2PKH || matchedP2SH)) {
763
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]");
764
                        }
765
766
                        // We are satisfied that the address is for the same
767
                        // destination, so substitute addressFromServer with our
768
                        // 'reencoded' form.
769
                        addressFromServer = decodeOurs.address;
770
                    }
771
772
                    // debug check
773
                    if (verifyAddress !== addressFromServer) {
774
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]");
775
                    }
776
                }
777
778
                return [addressFromServer, path];
779
            })
780
    );
781
782
    return deferred.promise;
783
};
784
785
/**
786
 * get the balance for the wallet
787
 *
788
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
789 View Code Duplication
 * @returns {q.Promise}
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
 */
791
Wallet.prototype.getBalance = function(cb) {
792
    var self = this;
793
794
    var deferred = q.defer();
795
    deferred.promise.spreadNodeify(cb);
796
797
    deferred.resolve(
798
        self.sdk.getWalletBalance(self.identifier)
799
            .then(function(result) {
800
                return [result.confirmed, result.unconfirmed];
801
            })
802
    );
803
804
    return deferred.promise;
805
};
806
807
/**
808
 * get the balance for the wallet
809
 *
810
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
811
 * @returns {q.Promise}
812
 */
813
Wallet.prototype.getInfo = function(cb) {
814
    var self = this;
815
816
    var deferred = q.defer();
817
    deferred.promise.spreadNodeify(cb);
818
819
    deferred.resolve(
820
        self.sdk.getWalletBalance(self.identifier)
821
    );
822
823
    return deferred.promise;
824
};
825
826
/**
827
 * do wallet discovery (slow)
828
 *
829
 * @param [gap] int             gap limit
830
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
831
 * @returns {q.Promise}
832
 */
833
Wallet.prototype.doDiscovery = function(gap, cb) {
834
    var self = this;
835
836
    if (typeof gap === "function") {
837
        cb = gap;
838
        gap = null;
839
    }
840
841
    var deferred = q.defer();
842
    deferred.promise.spreadNodeify(cb);
843
844
    deferred.resolve(
845
        self.sdk.doWalletDiscovery(self.identifier, gap)
846
            .then(function(result) {
847
                return [result.confirmed, result.unconfirmed];
848
            })
849
    );
850
851
    return deferred.promise;
852
};
853
854
/**
855
 *
856
 * @param [force]   bool            ignore warnings (such as non-zero balance)
857
 * @param [cb]      function        callback(err, success)
858
 * @returns {q.Promise}
859
 */
860
Wallet.prototype.deleteWallet = function(force, cb) {
861
    var self = this;
862
863
    if (typeof force === "function") {
864
        cb = force;
865
        force = false;
866
    }
867 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
    var deferred = q.defer();
869
    deferred.promise.nodeify(cb);
870
871
    if (self.locked) {
872
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
873
        return deferred.promise;
874
    }
875
876
    var checksum = self.primaryPrivateKey.getAddress();
877
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
878
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
879
880
    deferred.resolve(
881
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
882
            .then(function(result) {
883
                return result.deleted;
884
            })
885
    );
886
887
    return deferred.promise;
888
};
889
890
/**
891
 * create, sign and send a transaction
892
 *
893
 * @param pay                   array       {'address': (int)value}     coins to send
894
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
895
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
896
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
897
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
898
 * @param [twoFactorToken]      string      2FA token
899
 * @param options
900
 * @param [cb]                  function    callback(err, txHash)
901
 * @returns {q.Promise}
902
 */
903
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
904
905
    /* jshint -W071 */
906
    var self = this;
907
908
    if (typeof changeAddress === "function") {
909
        cb = changeAddress;
910
        changeAddress = null;
911
    } else if (typeof allowZeroConf === "function") {
912
        cb = allowZeroConf;
913
        allowZeroConf = false;
914
    } else if (typeof randomizeChangeIdx === "function") {
915
        cb = randomizeChangeIdx;
916
        randomizeChangeIdx = true;
917
    } else if (typeof feeStrategy === "function") {
918
        cb = feeStrategy;
919
        feeStrategy = null;
920
    } else if (typeof twoFactorToken === "function") {
921
        cb = twoFactorToken;
922
        twoFactorToken = null;
923
    } else if (typeof options === "function") {
924
        cb = options;
925
        options = {};
926
    }
927
928
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
929
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
930
    options = options || {};
931
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
932
933
    var deferred = q.defer();
934
    deferred.promise.nodeify(cb);
935
936
    if (self.locked) {
937
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
938
        return deferred.promise;
939
    }
940
941
    q.nextTick(function() {
942
        deferred.notify(Wallet.PAY_PROGRESS_START);
943
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
944
            .then(
945
            function(r) { return r; },
946
            function(e) { deferred.reject(e); },
947
            function(progress) {
948
                deferred.notify(progress);
949
            }
950
        )
951
            .spread(
952
            function(tx, utxos) {
953
954
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
955
956
                var data = {
957
                    signed_transaction: tx.toHex(),
958
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
959
                };
960
961
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
962
                    .then(function(result) {
963
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
964
965
                        if (!result || !result['complete'] || result['complete'] === 'false') {
966
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
967
                        } else {
968
                            return result['txid'];
969
                        }
970
                    });
971
            },
972
            function(e) {
973
                throw e;
974
            }
975
        )
976
            .then(
977
            function(r) { deferred.resolve(r); },
978
            function(e) { deferred.reject(e); }
979
        )
980
        ;
981
    });
982
983
    return deferred.promise;
984
};
985
986
Wallet.prototype.decodeAddress = function(address) {
987
    return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr);
988
};
989
990
function readBech32Address(address, network) {
991
    var addr;
992
    var err;
993
    try {
994
        addr = bitcoin.address.fromBech32(address, network);
995
        err = null;
996
997
    } catch (_err) {
998
        err = _err;
999
    }
1000
1001
    if (!err) {
1002
        // Valid bech32 but invalid network immediately alerts
1003
        if (addr.prefix !== network.bech32) {
1004
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1005
        }
1006
    }
1007
1008
    return [err, addr];
1009
}
1010
1011
function readCashAddress(address, network) {
1012
    var addr;
1013
    var err;
1014
    try {
1015
        addr = bitcoin.address.fromBase32(address);
1016
        err = null;
1017
    } catch (_err) {
1018
        err = _err;
1019
    }
1020
1021
    if (!err) {
1022
        // Valid base58 but invalid network immediately alerts
1023
        if (addr.prefix !== network.cashAddrPrefix) {
1024
            throw new Error(address + ' has an invalid prefix');
1025
        }
1026
    }
1027
1028
    return [err, addr];
1029
}
1030
1031
function readBase58Address(address, network) {
1032
    var addr;
1033
    var err;
1034
    try {
1035
        addr = bitcoin.address.fromBase58Check(address);
1036
        err = null;
1037
    } catch (_err) {
1038
        err = _err;
1039
    }
1040
1041
    if (!err) {
1042
        // Valid base58 but invalid network immediately alerts
1043
        if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
1044
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1045
        }
1046
    }
1047
1048
    return [err, addr];
1049
}
1050
1051
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
1052
    var addr;
1053
    var type;
1054
    var err;
1055
1056
    function readAddress(reader, readType) {
1057
        var decoded = reader(address, network);
1058
        if (decoded[0] === null) {
1059
            addr = decoded[1];
1060
            type = readType;
1061
        } else {
1062
            err = decoded[0];
1063
        }
1064
    }
1065
1066
    if (network === bitcoin.networks.bitcoin || network === bitcoin.networks.testnet) {
1067
        readAddress(readBech32Address, "bech32");
1068
    }
1069
1070
    if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
1071
        readAddress(readCashAddress, "cashaddr");
1072
    }
1073
1074
    if (!addr) {
1075
        readAddress(readBase58Address, "base58");
1076
    }
1077
1078
    if (addr) {
1079
        return {
1080
            address: address,
1081
            decoded: addr,
1082
            type: type
1083
        };
1084
    } else {
1085
        throw new blocktrail.InvalidAddressError(err.message);
1086
    }
1087
};
1088
1089
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
1090
    var send = [];
1091
1092
    var readFunc;
1093
1094
    // Deal with two different forms
1095
    if (Array.isArray(pay)) {
1096
        // output[]
1097
        readFunc = function(i, output, obj) {
1098
            if (typeof output !== "object") {
1099
                throw new Error("Invalid transaction output for numerically indexed list [1]");
1100
            }
1101
1102
            var keys = Object.keys(output);
1103
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
1104
                obj.scriptPubKey = output["scriptPubKey"];
1105
                obj.value = output["value"];
1106
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1107
                obj.address = output["address"];
1108
                obj.value = output["value"];
1109
            } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
1110
                obj.address = output[0];
1111
                obj.value = output[1];
1112
            } else {
1113
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1114
            }
1115
        };
1116
    } else if (typeof pay === "object") {
1117
        // map[addr]amount
1118
        readFunc = function(address, value, obj) {
1119
            obj.address = address.trim();
1120
            obj.value = value;
1121
            if (obj.address === Wallet.OP_RETURN) {
1122
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1123
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1124
                obj.value = 0;
1125
                obj.address = null;
1126
            }
1127
        };
1128
    } else {
1129
        throw new Error("Invalid input");
1130
    }
1131
1132
    Object.keys(pay).forEach(function(key) {
1133
        var obj = {};
1134
        readFunc(key, pay[key], obj);
1135
1136
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1137
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1138
        }
1139
1140
        // Remove address, replace with scriptPubKey
1141
        if (typeof obj.address === "string") {
1142
            try {
1143
                var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
1144
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
1145
                delete obj.address;
1146
            } catch (e) {
1147
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1148
            }
1149
        }
1150
1151
        // Extra checks when the output isn't OP_RETURN
1152
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1153
            if (!(obj.value = parseInt(obj.value, 10))) {
1154
                throw new blocktrail.WalletSendError("Values should be non zero");
1155
            } else if (obj.value <= blocktrail.DUST) {
1156
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1157
            }
1158
        }
1159
1160
        // Value fully checked now
1161
        obj.value = parseInt(obj.value, 10);
1162
1163
        send.push(obj);
1164
    });
1165
1166
    return send;
1167
};
1168
1169
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1170
    /* jshint -W071 */
1171
    var self = this;
1172
1173
    if (typeof changeAddress === "function") {
1174
        cb = changeAddress;
1175
        changeAddress = null;
1176
    } else if (typeof allowZeroConf === "function") {
1177
        cb = allowZeroConf;
1178
        allowZeroConf = false;
1179
    } else if (typeof randomizeChangeIdx === "function") {
1180
        cb = randomizeChangeIdx;
1181
        randomizeChangeIdx = true;
1182
    } else if (typeof feeStrategy === "function") {
1183
        cb = feeStrategy;
1184
        feeStrategy = null;
1185
    } else if (typeof options === "function") {
1186
        cb = options;
1187
        options = {};
1188
    }
1189
1190
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1191
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1192
    options = options || {};
1193
1194
    var deferred = q.defer();
1195
    deferred.promise.spreadNodeify(cb);
1196
1197
    q.nextTick(function() {
1198
        var send;
1199
        try {
1200
            send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1201
        } catch (e) {
1202
            deferred.reject(e);
1203
            return deferred.promise;
1204
        }
1205
1206
        if (!send.length) {
1207
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1208
            return deferred.promise;
1209
        }
1210
1211
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1212
1213
        deferred.resolve(
1214
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1215
            /**
1216
             *
1217
             * @param {Object[]} utxos
1218
             * @param fee
1219
             * @param change
1220
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1221
             * @returns {*}
1222
             */
1223
                .spread(function(utxos, fee, change) {
1224
                    var tx, txb, outputs = [];
1225
1226
                    var deferred = q.defer();
1227
1228
                    async.waterfall([
1229
                        /**
1230
                         * prepare
1231
                         *
1232
                         * @param cb
1233
                         */
1234
                        function(cb) {
1235
                            var inputsTotal = utxos.map(function(utxo) {
1236
                                return utxo['value'];
1237
                            }).reduce(function(a, b) {
1238
                                return a + b;
1239
                            });
1240
                            var outputsTotal = send.map(function(output) {
1241
                                return output.value;
1242
                            }).reduce(function(a, b) {
1243
                                return a + b;
1244
                            });
1245
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1246
1247
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1248
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1249
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1250
                            }
1251
1252
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1253
                        },
1254
                        /**
1255
                         * init transaction builder
1256
                         *
1257
                         * @param cb
1258
                         */
1259
                        function(cb) {
1260
                            txb = new bitcoin.TransactionBuilder(self.network);
1261
                            if (self.bitcoinCash) {
1262
                                txb.enableBitcoinCash();
1263
                            }
1264
1265
                            cb();
1266
                        },
1267
                        /**
1268
                         * add UTXOs as inputs
1269
                         *
1270
                         * @param cb
1271
                         */
1272
                        function(cb) {
1273
                            var i;
1274
1275
                            for (i = 0; i < utxos.length; i++) {
1276
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1277
                            }
1278
1279
                            cb();
1280
                        },
1281
                        /**
1282
                         * build desired outputs
1283
                         *
1284
                         * @param cb
1285
                         */
1286
                        function(cb) {
1287
                            send.forEach(function(_send) {
1288
                                if (_send.address) {
1289
                                    outputs.push({address: _send.address, value: _send.value});
1290
                                } else if (_send.scriptPubKey) {
1291
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1292
                                } else {
1293
                                    throw new Error("Invalid send");
1294
                                }
1295
                            });
1296
                            cb();
1297
                        },
1298
                        /**
1299
                         * get change address if required
1300
                         *
1301
                         * @param cb
1302
                         */
1303
                        function(cb) {
1304
                            if (change > 0) {
1305
                                if (change <= blocktrail.DUST) {
1306
                                    change = 0; // don't do a change output if it would be a dust output
1307
1308
                                } else {
1309
                                    if (!changeAddress) {
1310
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1311
1312
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1313
                                            if (err) {
1314
                                                return cb(err);
1315
                                            }
1316
                                            changeAddress = address;
1317
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1318
                                        });
1319
                                    }
1320
                                }
1321
                            }
1322
1323
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1324
                        },
1325
                        /**
1326
                         * add change to outputs
1327
                         *
1328
                         * @param cb
1329
                         */
1330
                        function(cb) {
1331
                            if (change > 0) {
1332
                                if (randomizeChangeIdx) {
1333
                                    outputs.splice(_.random(0, outputs.length), 0, {
1334
                                        address: changeAddress,
1335
                                        value: change
1336
                                    });
1337
                                } else {
1338
                                    outputs.push({address: changeAddress, value: change});
1339
                                }
1340
                            }
1341
1342
                            cb();
1343
                        },
1344
                        /**
1345
                         * add outputs to txb
1346
                         *
1347
                         * @param cb
1348
                         */
1349
                        function(cb) {
1350
                            outputs.forEach(function(outputInfo) {
1351
                                txb.addOutput(outputInfo.scriptPubKey || outputInfo.address, outputInfo.value);
1352
                            });
1353
1354
                            cb();
1355
                        },
1356
                        /**
1357
                         * sign
1358
                         *
1359
                         * @param cb
1360
                         */
1361
                        function(cb) {
1362
                            var i, privKey, path, redeemScript, witnessScript;
1363
1364
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1365
1366
                            for (i = 0; i < utxos.length; i++) {
1367
                                var mode = SignMode.SIGN;
1368
                                if (utxos[i].sign_mode) {
1369
                                    mode = utxos[i].sign_mode;
1370
                                }
1371
1372
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1373
                                witnessScript = null;
1374
                                if (mode === SignMode.SIGN) {
1375
                                    path = utxos[i]['path'].replace("M", "m");
1376
1377
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1378
                                    if (self.primaryPrivateKey) {
1379
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1380
                                    } else if (self.backupPrivateKey) {
1381
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1382
                                    } else {
1383
                                        throw new Error("No master privateKey present");
1384
                                    }
1385
1386
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1387
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1388
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1389
                                    }
1390
1391
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1392
                                    if (self.bitcoinCash) {
1393
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1394
                                    }
1395
1396
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1397
                                }
1398
                            }
1399
1400
                            tx = txb.buildIncomplete();
1401
1402
                            cb();
1403
                        },
1404
                        /**
1405
                         * estimate fee to verify that the API is not providing us wrong data
1406
                         *
1407
                         * @param cb
1408
                         */
1409
                        function(cb) {
1410
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1411
1412
                            if (self.sdk.feeSanityCheck) {
1413
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1414
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1415
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1416
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1417
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1418
                                        }
1419
                                    break;
1420
1421
                                    case Wallet.FEE_STRATEGY_HIGH_PRIORITY:
1422
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1423
                                    case Wallet.FEE_STRATEGY_LOW_PRIORITY:
1424
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1425
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1426
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1427
                                        }
1428
                                    break;
1429
                                }
1430
                            }
1431
1432
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1433
                        }
1434
                    ], function(err) {
1435
                        if (err) {
1436
                            deferred.reject(new blocktrail.WalletSendError(err));
1437
                            return;
1438
                        }
1439
1440
                        deferred.resolve([tx, utxos]);
1441
                    });
1442
1443
                    return deferred.promise;
1444
                }
1445
            )
1446
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1447
    });
1448
1449
    return deferred.promise;
1450
};
1451
1452
1453
/**
1454
 * use the API to get the best inputs to use based on the outputs
1455
 *
1456
 * @param pay               array       {'address': (int)value}     coins to send
1457
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1458
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1459
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1460
 * @param [options]         object
1461
 * @param [cb]              function    callback(err, utxos, fee, change)
1462
 * @returns {q.Promise}
1463
 */
1464
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1465
    var self = this;
1466
1467
    if (typeof lockUTXO === "function") {
1468
        cb = lockUTXO;
1469
        lockUTXO = true;
1470
    } else if (typeof allowZeroConf === "function") {
1471
        cb = allowZeroConf;
1472
        allowZeroConf = false;
1473
    } else if (typeof feeStrategy === "function") {
1474
        cb = feeStrategy;
1475
        feeStrategy = null;
1476
    } else if (typeof options === "function") {
1477
        cb = options;
1478
        options = {};
1479
    }
1480
1481
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1482
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1483
    options = options || {};
1484
1485
    var send;
1486
    try {
1487
        send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1488
    } catch (e) {
1489
        var deferred = q.defer();
1490
        deferred.promise.nodeify(cb);
1491
        deferred.reject(e);
1492
        return deferred.promise;
1493
    }
1494
1495
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1496
};
1497
1498
/**
1499
 * send the transaction using the API
1500
 *
1501
 * @param txHex             string      partially signed transaction as hex string
1502
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1503
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1504
 * @param [twoFactorToken]  string      2FA token
1505
 * @param prioboost         bool
1506
 * @param [cb]              function    callback(err, txHash)
1507
 * @returns {q.Promise}
1508
 */
1509
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1510
    var self = this;
1511
1512
    if (typeof twoFactorToken === "function") {
1513
        cb = twoFactorToken;
1514
        twoFactorToken = null;
1515
        prioboost = false;
1516
    } else if (typeof prioboost === "function") {
1517
        cb = twoFactorToken;
1518
        prioboost = false;
1519
    }
1520
1521
    var deferred = q.defer();
1522
    deferred.promise.nodeify(cb);
1523
1524
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1525
        .then(
1526
            function(result) {
1527
                deferred.resolve(result);
1528
            },
1529
            function(e) {
1530
                if (e.requires_2fa) {
1531
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1532
                } else if (e.message.match(/Invalid two_factor_token/)) {
1533
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1534
                } else {
1535
                    deferred.reject(e);
1536
                }
1537
            }
1538
        )
1539
    ;
1540
1541
    return deferred.promise;
1542
};
1543
1544
/**
1545
 * setup a webhook for this wallet
1546
 *
1547
 * @param url           string      URL to receive webhook events
1548
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1549
 * @param [cb]          function    callback(err, webhook)
1550
 * @returns {q.Promise}
1551
 */
1552
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1553
    var self = this;
1554
1555
    if (typeof identifier === "function") {
1556
        cb = identifier;
1557
        identifier = null;
1558
    }
1559
1560
    identifier = identifier || ('WALLET-' + self.identifier);
1561
1562
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1563
};
1564
1565
/**
1566
 * delete a webhook that was created for this wallet
1567
 *
1568
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1569
 * @param [cb]          function    callback(err, success)
1570
 * @returns {q.Promise}
1571
 */
1572
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1573
    var self = this;
1574
1575
    if (typeof identifier === "function") {
1576
        cb = identifier;
1577
        identifier = null;
1578
    }
1579
1580
    identifier = identifier || ('WALLET-' + self.identifier);
1581
1582
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1583
};
1584
1585
/**
1586
 * get all transactions for the wallet (paginated)
1587
 *
1588
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1589
 * @param [cb]      function    callback(err, transactions)
1590
 * @returns {q.Promise}
1591
 */
1592
Wallet.prototype.transactions = function(params, cb) {
1593
    var self = this;
1594
1595
    return self.sdk.walletTransactions(self.identifier, params, cb);
1596
};
1597
1598
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1599
    var self = this;
1600
1601
    if (typeof allowZeroConf === "function") {
1602
        cb = allowZeroConf;
1603
        allowZeroConf = false;
1604
    } else if (typeof feeStrategy === "function") {
1605
        cb = feeStrategy;
1606
        feeStrategy = null;
1607
    } else if (typeof options === "function") {
1608
        cb = options;
1609
        options = {};
1610
    }
1611
1612
    if (typeof allowZeroConf === "object") {
1613
        options = allowZeroConf;
1614
        allowZeroConf = false;
1615
    } else if (typeof feeStrategy === "object") {
1616
        options = feeStrategy;
1617
        feeStrategy = null;
1618
    }
1619
1620
    options = options || {};
1621
1622
    if (typeof options.allowZeroConf !== "undefined") {
1623
        allowZeroConf = options.allowZeroConf;
1624
    }
1625
    if (typeof options.feeStrategy !== "undefined") {
1626
        feeStrategy = options.feeStrategy;
1627
    }
1628
1629
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1630
1631
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1632
};
1633
1634
/**
1635
 * get all addresses for the wallet (paginated)
1636
 *
1637
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1638
 * @param [cb]      function    callback(err, addresses)
1639
 * @returns {q.Promise}
1640
 */
1641
Wallet.prototype.addresses = function(params, cb) {
1642
    var self = this;
1643
1644
    return self.sdk.walletAddresses(self.identifier, params, cb);
1645
};
1646
1647
/**
1648
 * @param address   string      the address to label
1649
 * @param label     string      the label
1650
 * @param [cb]      function    callback(err, res)
1651
 * @returns {q.Promise}
1652
 */
1653
Wallet.prototype.labelAddress = function(address, label, cb) {
1654
    var self = this;
1655
1656
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1657
};
1658
1659
/**
1660
 * get all UTXOs for the wallet (paginated)
1661
 *
1662
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1663
 * @param [cb]      function    callback(err, addresses)
1664
 * @returns {q.Promise}
1665
 */
1666
Wallet.prototype.utxos = function(params, cb) {
1667
    var self = this;
1668
1669
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1670
};
1671
1672
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1673
1674
/**
1675
 * sort list of pubkeys to be used in a multisig redeemscript
1676
 *  sorted in lexicographical order on the hex of the pubkey
1677
 *
1678
 * @param pubKeys   {bitcoin.HDNode[]}
1679
 * @returns string[]
1680
 */
1681
Wallet.sortMultiSigKeys = function(pubKeys) {
1682
    pubKeys.sort(function(key1, key2) {
1683
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1684
    });
1685
1686
    return pubKeys;
1687
};
1688
1689
/**
1690
 * determine how much fee is required based on the inputs and outputs
1691
 *  this is an estimation, not a proper 100% correct calculation
1692
 *
1693
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1694
 * @param {bitcoin.Transaction} tx
1695
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1696
 * @returns {number}
1697
 */
1698
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1699
    var size = Wallet.estimateIncompleteTxSize(tx);
1700
    var sizeKB = size / 1000;
1701
    var sizeKBCeil = Math.ceil(size / 1000);
1702
1703
    if (feePerKb) {
1704
        return parseInt(sizeKB * feePerKb, 10);
1705
    } else {
1706
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1707
    }
1708
};
1709
1710
/**
1711
 * Takes tx and utxos, computing their estimated vsize,
1712
 * and uses feePerKb (or BASEFEE as default) to estimate
1713
 * the number of satoshis in fee.
1714
 *
1715
 * @param {bitcoin.Transaction} tx
1716
 * @param {Array} utxos
1717
 * @param feePerKb
1718
 * @returns {Number}
1719
 */
1720
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1721
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1722
    var sizeKB = vsize / 1000;
1723
    var sizeKBCeil = Math.ceil(vsize / 1000);
1724
1725
    if (feePerKb) {
1726
        return parseInt(sizeKB * feePerKb, 10);
1727
    } else {
1728
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1729
    }
1730
};
1731
1732
/**
1733
 * determine how much fee is required based on the inputs and outputs
1734
 *  this is an estimation, not a proper 100% correct calculation
1735
 *
1736
 * @param {bitcoin.Transaction} tx
1737
 * @returns {number}
1738
 */
1739
Wallet.estimateIncompleteTxSize = function(tx) {
1740
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1741
1742
    size += tx.outs.length * 34;
1743
1744
    tx.ins.forEach(function(txin) {
1745
        var scriptSig = txin.script,
1746
            scriptType = bitcoin.script.classifyInput(scriptSig);
1747
1748
        var multiSig = [2, 3]; // tmp hardcoded
1749
1750
        // Re-classify if P2SH
1751
        if (!multiSig && scriptType === 'scripthash') {
1752
            var sigChunks = bitcoin.script.decompile(scriptSig);
1753
            var redeemScript = sigChunks.slice(-1)[0];
1754
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1755
            scriptType = bitcoin.script.classifyInput(scriptSig);
1756
1757
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1758
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1759
            }
1760
1761
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1762
            if (scriptType === 'multisig') {
1763
                var rsChunks = bitcoin.script.decompile(redeemScript);
1764
                var mOp = rsChunks[0];
1765
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1766
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1767
                }
1768
1769
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1770
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1771
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1772
                }
1773
1774
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1775
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1776
                if (n < m) {
1777
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1778
                }
1779
1780
                multiSig = [m, n];
1781
            }
1782
        }
1783
1784
        if (multiSig) {
1785
            size += (
1786
                32 + // txhash
1787
                4 + // idx
1788
                3 + // scriptVarInt[>=253]
1789
                1 + // OP_0
1790
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1791
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1792
                4 // sequence
1793
            );
1794
1795
        } else {
1796
            size += 32 + // txhash
1797
                4 + // idx
1798
                73 + // sig
1799
                34 + // script
1800
                4; // sequence
1801
        }
1802
    });
1803
1804
    return size;
1805
};
1806
1807
/**
1808
 * determine how much fee is required based on the amount of inputs and outputs
1809
 *  this is an estimation, not a proper 100% correct calculation
1810
 *  this asumes all inputs are 2of3 multisig
1811
 *
1812
 * @todo: mark deprecated in favor of situations where UTXOS are known
1813
 * @param txinCnt       {number}
1814
 * @param txoutCnt      {number}
1815
 * @returns {number}
1816
 */
1817
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1818
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1819
1820
    size += txoutCnt * 34;
1821
1822
    size += (
1823
            32 + // txhash
1824
            4 + // idx
1825
            3 + // scriptVarInt[>=253]
1826
            1 + // OP_0
1827
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1828
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1829
            4 // sequence
1830
        ) * txinCnt;
1831
1832
    var sizeKB = Math.ceil(size / 1000);
1833
1834
    return sizeKB * blocktrail.BASE_FEE;
1835
};
1836
1837
/**
1838
 * create derived key from parent key by path
1839
 *
1840
 * @param hdKey     {bitcoin.HDNode}
1841
 * @param path      string
1842
 * @param keyPath   string
1843
 * @returns {bitcoin.HDNode}
1844
 */
1845
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1846
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1847
1848
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1849
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1850
    }
1851
1852
    if (path[0] === "m" && keyPath[0] === "M") {
1853
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1854
    }
1855
1856
    // if the desired path is public while the input is private
1857
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1858
    if (toPublic) {
1859
        // derive the private path, convert to public when returning
1860
        path[0] = "m";
1861
    }
1862
1863
    // keyPath should be the parent parent of path
1864
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1865
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1866
    }
1867
1868
    // remove the part of the path we already have
1869
    path = path.substr(keyPath.length);
1870
1871
    // iterate over the chunks and derive
1872
    var newKey = hdKey;
1873
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1874
        if (!chunk) {
1875
            return;
1876
        }
1877
1878
        if (chunk.indexOf("'") !== -1) {
1879
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1880
        }
1881
1882
        newKey = newKey.derive(parseInt(chunk, 10));
1883
    });
1884
1885
    if (toPublic) {
1886
        return newKey.neutered();
1887
    } else {
1888
        return newKey;
1889
    }
1890
};
1891
1892
module.exports = Wallet;
1893